import { EntityMetadata } from "../metadata/EntityMetadata"
import { DataSource } from "../data-source/DataSource"
import { RelationMetadata } from "../metadata/RelationMetadata"
import { QueryBuilderUtils } from "./QueryBuilderUtils"
import { QueryExpressionMap } from "./QueryExpressionMap"
import { Alias } from "./Alias"
import { ObjectUtils } from "../util/ObjectUtils"
import { TypeORMError } from "../error"
import { DriverUtils } from "../driver/DriverUtils"

/**
 * Stores all join attributes which will be used to build a JOIN query.
 */
export class JoinAttribute {
    // -------------------------------------------------------------------------
    // Public Properties
    // -------------------------------------------------------------------------

    /**
     * Join direction.
     */
    direction: "LEFT" | "INNER"

    /**
     * Alias of the joined (destination) table.
     */
    alias: Alias

    /**
     * Joined table, entity target, or relation in "post.category" format.
     */
    entityOrProperty: Function | string

    /**
     * Extra condition applied to "ON" section of join.
     */
    condition?: string

    /**
     * Property + alias of the object where to joined data should be mapped.
     */
    mapToProperty?: string

    /**
     * Indicates if user maps one or many objects from the join.
     */
    isMappingMany?: boolean

    /**
     * Useful when the joined expression is a custom query to support mapping.
     */
    mapAsEntity?: Function | string

    // -------------------------------------------------------------------------
    // Constructor
    // -------------------------------------------------------------------------

    constructor(
        private connection: DataSource,
        private queryExpressionMap: QueryExpressionMap,
        joinAttribute?: JoinAttribute,
    ) {
        if (joinAttribute) {
            ObjectUtils.assign(this, joinAttribute)
        }
    }

    // -------------------------------------------------------------------------
    // Public Methods
    // -------------------------------------------------------------------------

    get isMany(): boolean {
        if (this.isMappingMany !== undefined) return this.isMappingMany

        if (this.relation)
            return this.relation.isManyToMany || this.relation.isOneToMany

        return false
    }

    isSelectedCache: boolean
    isSelectedEvaluated: boolean = false
    /**
     * Indicates if this join is selected.
     */
    get isSelected(): boolean {
        if (!this.isSelectedEvaluated) {
            let getValue = () => {
                for (const select of this.queryExpressionMap.selects) {
                    if (select.selection === this.alias.name) return true

                    if (
                        this.metadata &&
                        !!this.metadata.columns.find(
                            (column) =>
                                select.selection ===
                                this.alias.name + "." + column.propertyPath,
                        )
                    )
                        return true
                }

                return false
            }
            this.isSelectedCache = getValue()
            this.isSelectedEvaluated = true
        }
        return this.isSelectedCache
    }

    /**
     * Name of the table which we should join.
     */
    get tablePath(): string {
        return this.metadata
            ? this.metadata.tablePath
            : (this.entityOrProperty as string)
    }

    /**
     * Alias of the parent of this join.
     * For example, if we join ("post.category", "categoryAlias") then "post" is a parent alias.
     * This value is extracted from entityOrProperty value.
     * This is available when join was made using "post.category" syntax.
     */
    get parentAlias(): string | undefined {
        if (!QueryBuilderUtils.isAliasProperty(this.entityOrProperty))
            return undefined

        return this.entityOrProperty.substr(
            0,
            this.entityOrProperty.indexOf("."),
        )
    }

    /**
     * Relation property name of the parent.
     * This is used to understand what is joined.
     * For example, if we join ("post.category", "categoryAlias") then "category" is a relation property.
     * This value is extracted from entityOrProperty value.
     * This is available when join was made using "post.category" syntax.
     */
    get relationPropertyPath(): string | undefined {
        if (!QueryBuilderUtils.isAliasProperty(this.entityOrProperty))
            return undefined

        return this.entityOrProperty.substr(
            this.entityOrProperty.indexOf(".") + 1,
        )
    }

    relationCache: RelationMetadata | undefined
    relationEvaluated: boolean = false
    /**
     * Relation of the parent.
     * This is used to understand what is joined.
     * This is available when join was made using "post.category" syntax.
     * Relation can be undefined if entityOrProperty is regular entity or custom table.
     */
    get relation(): RelationMetadata | undefined {
        if (!this.relationEvaluated) {
            let getValue = () => {
                if (!QueryBuilderUtils.isAliasProperty(this.entityOrProperty))
                    return undefined

                const relationOwnerSelection =
                    this.queryExpressionMap.findAliasByName(this.parentAlias!)
                let relation =
                    relationOwnerSelection.metadata.findRelationWithPropertyPath(
                        this.relationPropertyPath!,
                    )

                if (relation) {
                    return relation
                }

                if (relationOwnerSelection.metadata.parentEntityMetadata) {
                    relation =
                        relationOwnerSelection.metadata.parentEntityMetadata.findRelationWithPropertyPath(
                            this.relationPropertyPath!,
                        )
                    if (relation) {
                        return relation
                    }
                }

                throw new TypeORMError(
                    `Relation with property path ${this.relationPropertyPath} in entity was not found.`,
                )
            }
            this.relationCache = getValue.bind(this)()
            this.relationEvaluated = true
        }
        return this.relationCache
    }

    /**
     * Metadata of the joined entity.
     * If table without entity was joined, then it will return undefined.
     */
    get metadata(): EntityMetadata | undefined {
        // entityOrProperty is relation, e.g. "post.category"
        if (this.relation) return this.relation.inverseEntityMetadata

        // entityOrProperty is Entity class
        if (this.connection.hasMetadata(this.entityOrProperty))
            return this.connection.getMetadata(this.entityOrProperty)

        // Overriden mapping entity provided for leftJoinAndMapOne with custom query builder
        if (this.mapAsEntity && this.connection.hasMetadata(this.mapAsEntity)) {
            return this.connection.getMetadata(this.mapAsEntity)
        }

        return undefined

        /*if (typeof this.entityOrProperty === "string") { // entityOrProperty is a custom table

            // first try to find entity with such name, this is needed when entity does not have a target class,
            // and its target is a string name (scenario when plain old javascript is used or entity schema is loaded from files)
            const metadata = this.connection.entityMetadatas.find(metadata => metadata.name === this.entityOrProperty);
            if (metadata)
                return metadata;

            // check if we have entity with such table name, and use its metadata if found
            return this.connection.entityMetadatas.find(metadata => metadata.tableName === this.entityOrProperty);
        }*/
    }

    /**
     * Generates alias of junction table, whose ids we get.
     */
    get junctionAlias(): string {
        if (!this.relation) {
            throw new TypeORMError(
                `Cannot get junction table for join without relation.`,
            )
        }
        if (typeof this.entityOrProperty !== "string") {
            throw new TypeORMError(`Junction property is not defined.`)
        }

        const aliasProperty = this.entityOrProperty.substr(
            0,
            this.entityOrProperty.indexOf("."),
        )

        if (this.relation.isOwning) {
            return DriverUtils.buildAlias(
                this.connection.driver,
                undefined,
                aliasProperty,
                this.alias.name,
            )
        } else {
            return DriverUtils.buildAlias(
                this.connection.driver,
                undefined,
                this.alias.name,
                aliasProperty,
            )
        }
    }

    get mapToPropertyParentAlias(): string | undefined {
        if (!this.mapToProperty) return undefined

        return this.mapToProperty!.split(".")[0]
    }

    get mapToPropertyPropertyName(): string | undefined {
        if (!this.mapToProperty) return undefined

        return this.mapToProperty!.split(".")[1]
    }
}
